Profile picture

[k8s] Django 기반 서비스 배포하기 (Feat: nfs / pv,pvc / configmap)

JaehyoJJAng2023년 06월 10일

개요

쿠버네티스로 Pinterest 기반의 웹 서비스를 배포해보려고 한다.

여기서 Pod 간 NFS 연동, PV-PVC, ConfigMap 등을 종합적으로 사용해 볼 것이다.


프로젝트 구조

Django 개발 서버 디렉토리 구조

├── apps
│   ├── accountapp
│   ├── articleapp
│   ├── commentapp
│   ├── dislikeapp
│   ├── Dockerfile
│   ├── likeapp
│   ├── manage.py
│   ├── media
│   ├── pragmatic
│   ├── profileapp
│   ├── projectapp
│   ├── requirements.txt
│   ├── static
│   ├── staticfiles
│   ├── subscribeapp
│   └── templates
├── configMap
│   └── django-config.yaml
├── k8s-db-deployment.yaml
├── k8s-django-deployment.yaml
├── k8s-nginx-deployment.yaml
└── pv-pvc
    ├── db-pv-pvc.yaml
    ├── nginx-media-pv-pvc.yaml
    ├── nginx-static-pv-pvc.yaml
    ├── web-media-pv-pvc.yaml
    └── web-static-pv-pvc.yaml

사전 준비

외부로 배포하기 위해 Nginx Ingress Controller, metalLB 설치 필요함.


PV 외부 스토리지를 nfs로 설정할 것이기에 내부망에 NFS 서버가 구축되어 있어야 함.



postgresql DB 접속을 위해 django의 settings.py 파일에서 DATABASES 변수 값을 아래와 같이 수정

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('POSTGRES_DB','postgres'),
        'USER': os.getenv('POSTGRES_USER','postgres'),
        'PASSWORD': os.getenv('POSTGRES_PASSWORD','postgres'),
        'HOST': os.getenv('POSTGRES_HOST','postgres-service'),
        'PORT': int(os.getenv('POSTGRES_PORT',5432))
    }
}

프로젝트 생성

  • 해당 챕터의 작업들은 모두 배포 서버(k8s)가 아닌 개발 서버에서 진행

Django 프로젝트 생성

# 가상환경 활성화
pyenv virtualenv 3.11.6 py3_11_6
pyenv activate py3_11_6

# Django 설치
pip install django

# 장고 프로젝트 생성
django-admin startproject myapp .

도커 이미지 생성 & 빌드

  • 개발한 Django 앱을 Dockerhub에 배포
  • postgresql은 빌드된 이미지 없이 순정 이미지를 pull 받아서 띄울 것임.

django

Dockerfile 작성

# pull official base image
FROM python:3.8-slim-buster

# set work directory
WORKDIR /usr/src/app

# set environment variable
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONBUFFERED 1

COPY . .

# install dependencies
RUN pip install --upgrade pip && pip install -r requirements.txt
RUN python manage.py migrate
CMD ["gunicorn", "pragmatic.wsgi:application", "--bind", "0.0.0.0:8000", "--reload"]

이미지 배포

  • 이미지 registry 서버를 따로 구축하여 해당 서버로부터 이미지를 pull 받아오도록 하였음.

이미지 빌드

docker build --tag xxxx/k8s-pinterest .

레지스트리 서버로 이미지 배포 위해 이미지 태그 변경

docker tag xxxx/k8s-pinterest <registry_서버_IP>:5000/k8s-pinterest

레지스트리 서버로 이미지 배포

docker push <registry_서버_IP>:5000/k8s-pinterest

서비스 배포하기

  • 해당 챕터의 작업들은 k8s Control Plane 서버에서 진행

PV, PVC 생성

Django, PostgreSQL 파드들의 데이터를 위한 스토리지를 설정하자.

이 때 PV는 NFS로 설정한다.


참고로 nginx는 이미 생성된 app-pv-pvc.yaml의 PV,PVC로 설정하도록 함.

  • Django 앱의 staticfiles, media에 대한 파일 서빙이 필요함.

코드를 작성해보자.


DB용: db-pv-pvc.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: db-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  nfs:
    server: 192.168.219.179
    path: /nfs/share/db
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

APP용: app-pv-pvc.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: web-static-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.219.179
    path: /nfs/share/web/staticfiles
  storageClassName: static-vol
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: web-static-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  storageClassName: static-vol
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: web-media-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 192.168.219.179
    path: /nfs/share/web/media
  storageClassName: media-vol
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: web-media-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  storageClassName: media-vol

ConfigMap 생성

Django와 PostgreSQL의 환경변수를 설정하자.

configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: django-config 
data:
  DEBUG: "1"
  SECRET_KEY: "XX"
  DJANGO_ALLOWED_HOSTS: "localhost 127.0.0.1 192.168.219.0/24 [::1]"
  SQL_ENGINE: "django.db.backends.postgresql"
  SQL_DATABASE: "pragmatic"
  SQL_USER: "pragmatic_user"
  SQL_PASSWORD: "pragmatic_pass"
  SQL_HOST: "db"
  SQL_PORT: "5432"
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config 
data:
  POSTGRES_USER: "pragmatic_user"
  POSTGRES_PASSWORD: "pragmatic_pass"
  POSTGRES_DB: "pragmatic"

PostgreSQL Deployment 생성

PostgreSQL 데이터베이스 설정이다.

k8s-db-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:12.0-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_USER
              valueFrom:
                configMapKeyRef:
                  name: postgres-config
                  key: POSTGRES_USER
            - name: POSTGRES_PASSWORD
              valueFrom:
                configMapKeyRef:
                  name: postgres-config
                  key: POSTGRES_PASSWORD
            - name: POSTGRES_DB
              valueFrom:
                configMapKeyRef:
                  name: postgres-config
                  key: POSTGRES_DB
          volumeMounts:
            - name: db-data
              mountPath: /var/lib/postgresql/data
      volumes:  # This section must align properly under 'spec'
        - name: db-data
          persistentVolumeClaim:
            claimName: db-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  selector:
    app: postgres
  ports:
    - protocol: TCP
      port: 5432
      targetPort: 5432
  clusterIP: None # StatefulSets와 비슷하게 Pod 간 직접 통신 지원

Django Deployment 생성

Django 애플리케이션 설정이다.

k8s-app-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: django
spec:
  replicas: 2
  selector:
    matchLabels:
      app: django
  template:
    metadata:
      labels:
        app: django
    spec:
      containers:
        - name: django
          image: yshrim12/pinterest
          ports:
            - containerPort: 8000
          env:
            - name: DEBUG
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: DEBUG
            - name: SECRET_KEY
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: SECRET_KEY
            - name: DJANGO_ALLOWED_HOSTS
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: DJANGO_ALLOWED_HOSTS
            - name: SQL_ENGINE
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: SQL_ENGINE
            - name: SQL_DATABASE
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: SQL_DATABASE
            - name: SQL_USER
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: SQL_USER
            - name: SQL_PASSWORD
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: SQL_PASSWORD
            - name: SQL_HOST
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: SQL_HOST
            - name: SQL_PORT
              valueFrom:
                configMapKeyRef:
                  name: django-config
                  key: SQL_PORT 
          volumeMounts:
            - name: static-data
              mountPath: /usr/src/app/staticfiles
            - name: media-data
              mountPath: /usr/src/app/media
      volumes:
        - name: static-data
          persistentVolumeClaim:
            claimName: web-static-pvc
        - name: media-data
          persistentVolumeClaim:
            claimName: web-media-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: django
  ports:
    - protocol: TCP
      port: 8000
      targetPort: 8000

Nginx Deployment 생성

Nginx 설정이다.

k8s-nginx-deployment.yaml

# nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: nginx.conf
        - name: static-data
          mountPath: /usr/src/app/staticfiles
        - name: media-data
          mountPath: /usr/src/app/media
      volumes:
      - name: nginx-config
        configMap:
          name: nginx-config
      - name: static-data
        persistentVolumeClaim:
          claimName: web-static-pvc
      - name: media-data
        persistentVolumeClaim:
          claimName: web-media-pvc
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  nginx.conf: |
    upstream pragmatic {
        server web:8000;
    }

    server {
        listen 80;
        client_max_body_size 0;

        location / {
            proxy_pass http://pragmatic;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_redirect off;
        }

        location /static/ {
            alias /usr/src/app/staticfiles/;
        }

        location /media/ {
            alias /usr/src/app/media/;
        }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 30001
  type: NodePort

nginx에서도 web-static-pvcweb-media-pvc를 사용하도록 하자.



data:
  nginx.conf: |
    upstream pragmatic {
        server web:8000;
    }

    server {
        listen 80;
        client_max_body_size 0;

        location / {
            proxy_pass http://pragmatic;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_redirect off;
        }

        location /static/ {
            alias /usr/src/app/staticfiles/;
        }

        location /media/ {
            alias /usr/src/app/media/;
        }
    }
---

nginx에서 django 앱의 /static/, /media/ 경로에 대해 정적 파일 서빙을 해주기 위해서는

django 파드 내에 있는 /usr/src/app/staticfiles, /usr/src/app/media 경로에 파일들이 필요한데

이때 nginx에서도 web-static-pvcweb-media-pvc를 사용하여

NFS 서버를 통해 해당 파일들을 nginx 파드로 넘겨줄 수 있도록 하는거다.


그리고 data.nginx.conf의 설정 값 중에,

upstream 섹션의 server web:8000; 이 부분은 django 앱의 서비스 명세서 이름을 지정한 것이다.

이는 쿠버네티스의 서비스 디스커버리(Service Discovery)에 대해 알고 있다면 무슨 뜻인지 이해가 갈 것이다.


리소스 적용

모든 YAML 파일을 클러스터에 적용하자.

# ConfigMap 적용
kubectl apply -f ./configmap

# pv-pvc 적용
kubectl apply -f ./pv-pvc

# deployment 적용
kubectl apply -f .

여기서 ./configmap은 configmap이 모여있는 경로이고,

./pv-pvc는 pv-pvc 명세서가 모여있는 경로이다.


접속 테스트

image


인그레스(Ingress) 적용

Ingress 실습 & helm으로 ingress-nginx-controller 설치하기 참고하기


트러블슈팅

watch 명령으로 Pod 간 STATUS에 이상이 없는지 모니터링

watch kubectl get pods -o wide

Pod 로그/이벤트 조회

kubectl describe pod/<pod_name>

413 Request entity too large

nginx-ingress에서 413 Request entity too large 에러 발생.
413 Request entity too large 해결하기 - k8s/ingress


Operation not permitted

kubectl get pods 명령을 실행하니 Postgresql 파드가 정상적으로 실행되고 있지 않는 문제가 발생함.

kubectl describe pod/<db-pod-id>  

Events:
  ...
  ...
  Warning  BackOff    10s (x7 over 89s)  kubelet            Back-off restarting failed container postgres in pod portfolio-postgres-7c87668c5f-4bzhj_default(a01f0f67-7e0e-428e-8451-5dbe9b3f126a)

DB 파드의 로그를 확인해보니

kubectl logs pod/<db-pod-id>

chown: /var/lib/postgresql/data: Operation not permitted

위와 같은 로그가 뜨는 중 ..


검색 결과 NFS 서버 설정에서 root 사용자가 nobody로 매핑되어 있어 chown이 차단되는 문제였음.

이를 방지하려면 /etc/exportfs에서 no_root_squash 옵션을 추가하라고 함.


NFS 서버로 이동한 후 /etc/exportfs 파일을 열고 다음과 같이 수정하였음.

/nfs/portfolio 192.168.219.0/24(rw,sync,no_subtree_check,no_root_squash)

그리고 변경 사항 적용

exportfs -ra
systemctl restart nfs-server

k8s control plane 이동 후 deployment 재시작하니 정상 작동 됨.


Loading script...